package com.hibase.core.api.component;

import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.util.concurrent.RateLimiter;
import com.hibase.common.constant.RedisKeyConstant;
import com.hibase.common.exception.HibaseAssert;
import com.hibase.common.exception.HibaseException;
import com.hibase.core.api.annotation.HibaseApi;
import com.hibase.core.api.config.HibaseApiTrafficRangeProperties;
import com.hibase.core.api.constant.HibaseApiProperties;
import com.hibase.core.api.entity.account.MsdApiAccount;
import com.hibase.core.api.entity.permission.MsdApiPermission;
import com.hibase.core.api.service.account.MsdApiAccountService;
import com.hibase.core.api.service.permission.MsdApiPermissionService;
import com.hibase.core.redis.util.RedisUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;

import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

/**
 * 系统拦截白名单下有部分接口是公开的，便于外部查询数据，这些接口均需要传输accessKey作为访问钥匙，如下有三种适用场景
 *
 * <p>没有传输该参数值可第一时间打回；</p>
 * <p>创建错误的accessKey校验直接打回；</p>
 * <p>传输正确的accessKey，查询账号相关信息，然后进行查询操作；</p>
 * <p>
 * api 鉴权切面
 * 支持方法上
 * <p>
 * 备注:
 * <li>2022年2月24日 新增流控</li>
 *
 * @author chenfeng
 * @version 1.0
 * @date 2021/8/30 10:13
 * @des https://gitee.com/cheershi
 * @remarks:
 */
@Slf4j
@Aspect
@Component
public class HibaseApiAspect {
    /**
     * key: 方法签名
     */
    private static final ConcurrentMap<String, RateLimiter> rateLimiterCache = new ConcurrentHashMap<>();

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HibaseApiTrafficRangeProperties trafficRangeProperties;

    @Autowired
    private MsdApiAccountService msdApiAccountService;

    @Autowired
    private MsdApiPermissionService msdApiPermissionService;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private AuthEnable authEnable;


    @SneakyThrows
    @Around("@within(hibaseApi) || @annotation(hibaseApi)")
    public Object around(ProceedingJoinPoint point, HibaseApi hibaseApi) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        // 流控
        rateLimiter(point.getSignature().toString(), hibaseApi);

        long currentTimeMillis = System.currentTimeMillis();

        // 跳过密钥校验
        if (hibaseApi.ignoreSignature()) {
            return point.proceed();
        }


        // step 1.获取请求头中的 appid 、 timestamp 、nonce 、sign
        String appId = obtainRequestValue(HibaseApiProperties.APP_ID);
        String timestamp = obtainRequestValue(HibaseApiProperties.TIME_STAMP);
        String nonce = obtainRequestValue(HibaseApiProperties.NONCE);
        String sign = obtainRequestValue(HibaseApiProperties.SIGN);
        log.debug("[Hibase API Check Authorization] API Type: {} ; app id: {}; timestamp: {}; nonce: {}; sign :{}", hibaseApi.type(), appId, timestamp, nonce, sign);

        // step 2. 参数完整校验
        HibaseAssert.isTrue(StrUtil.isAllNotBlank(appId, timestamp, nonce, sign), "Incomplete parameter :  missing parameter 【appId,timestamp,nonce,sign】");

        // step 3. valid timestamp
        long requestTimestamp = 0;
        if (NumberUtil.isLong(timestamp)) {
            if (StrUtil.length(timestamp) == 10) {
                requestTimestamp = Long.valueOf(timestamp) * 1000;
            } else if (StrUtil.length(timestamp) == 13) {
                requestTimestamp = Long.valueOf(timestamp);
            } else {
                throw new HibaseException("Please use a properly formatted timestamp : 10 digits or 13 digits");
            }
        }

        long pastTimestamp = currentTimeMillis - trafficRangeProperties.getPastTime();
        long futureTimestamp = currentTimeMillis + trafficRangeProperties.getFutureTime();

        // pastTimestamp<= requestTimestamp <= futureTimestamp
        HibaseAssert.isTrue(pastTimestamp <= requestTimestamp && requestTimestamp <= futureTimestamp, "Timestamp INVALID");

        // step 4. 获取appId 对应秘钥 appsecret
        MsdApiAccount result = (MsdApiAccount) redisUtil.get(RedisKeyConstant.CACHE_API_KEY + appId);
        if (ObjectUtil.isEmpty(result)) {
            result = msdApiAccountService.getOne(Wrappers.<MsdApiAccount>lambdaQuery().eq(MsdApiAccount::getAppid, appId));
            HibaseAssert.notNull(result, "The user name or password is incorrect");
            redisUtil.setEx(RedisKeyConstant.CACHE_API_KEY + appId, result, 24 * 60 * 60, TimeUnit.SECONDS);
        }
        // step 5. 数据验证 sign =  appid+appsecret+timestamp+nonce+请求的JSON报文通过SHA-1加密得到的签名值
        String requestJson = null;
        Object[] params = point.getArgs();
        // 检验幂等性
        /*
        获取方法，此处可将signature强转为MethodSignature
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        */

        Object o = redisUtil.get(RedisKeyConstant.CACHE_API_IDEMPOTENCE_KEY.concat(method.getName()).concat(StrUtil.COLON).concat(sign));
        if (ObjectUtil.isEmpty(o)) {
            redisUtil.setEx(RedisKeyConstant.CACHE_API_IDEMPOTENCE_KEY.concat(method.getName()).concat(StrUtil.COLON).concat(sign), sign, 3 * 60, TimeUnit.SECONDS);
        } else {
            throw new HibaseException("Do not submit twice");
        }
        //参数注解，1维是参数，2维是注解
        Annotation[][] annotations = method.getParameterAnnotations();
        for (int i = 0; i < annotations.length; i++) {
            Object param = params[i];
            Annotation[] paramAnn = annotations[i];
            //参数为空，直接下一个参数
            if (param == null || paramAnn.length == 0) {
                continue;
            }
            for (Annotation annotation : paramAnn) {
                //这里判断当前注解是否为 RequestBody.class
                if (annotation.annotationType().equals(RequestBody.class)) {
                    if (param instanceof String) {
                        requestJson = (String) param;
                    } else {
                        // 有问题 找韫磊
                        requestJson = JSON.toJSONString(param);
                    }
                    break;
                }
            }
        }
        HibaseAssert.notNull(requestJson, "Please confirm the request body");
        // todo 加密校验 可以根据自定义扩展
        String contrastSign = DigestUtils.sha1Hex(StrUtil.concat(true, appId, result.getAppsecret(), timestamp, nonce, requestJson));
        log.debug("当前签名：" + contrastSign);
        HibaseAssert.isTrue(contrastSign.equals(sign), "Authentication failure");
        // step 6. 验证账号是否可以请求当前接口
        MsdApiPermission permission = (MsdApiPermission) redisUtil.get(RedisKeyConstant.CACHE_API_PERMISSION_KEY + result.getId());
        if (ObjectUtil.isEmpty(permission)) {
            MsdApiPermission apiPermission = msdApiPermissionService.getOne(Wrappers.<MsdApiPermission>lambdaQuery().eq(MsdApiPermission::getAccountId, result.getId()).eq(MsdApiPermission::getMsdApiType, hibaseApi.type()));
            HibaseAssert.notNull(apiPermission, "Without permission");
            redisUtil.setEx(RedisKeyConstant.CACHE_API_PERMISSION_KEY + result.getId(), apiPermission, 24 * 60 * 60, TimeUnit.SECONDS);
        }
        return point.proceed();
    }

    private String obtainRequestValue(String paramKey) {
        return request.getHeader(paramKey);
    }


    private void rateLimiter(String key, HibaseApi hibaseApi) {
        if (hibaseApi != null && hibaseApi.limit() > 0) {
            double limit = hibaseApi.limit();
            if (rateLimiterCache.get(key) == null) {
                rateLimiterCache.put(key, RateLimiter.create(limit));
            }

            log.info("限流设置为: " + rateLimiterCache.get(key).getRate());
            // 尝试获取令牌
            if (rateLimiterCache.get(key) != null && !rateLimiterCache.get(key).tryAcquire(hibaseApi.timeout(), hibaseApi.timeUnit())) {
                log.info("限流了...");
                throw new HibaseException("service temporarily unavailable.");
            }
        }
    }
}
